# $Id: CRMConnector.pm 5 2006-09-05 19:05:09Z anbu78 $
#
#
#
package CRMConnector;

use Data::Dumper;
use strict;
use Carp;
use CGI qw(:standard);

use vars qw(
        $VERSION $SF_URI $SF_PREFIX $SF_PROXY $SF_SOBJECT_URI
);

$VERSION = '0.01';

$SF_PROXY = 'https://www.salesforce.com/services/Soap/u/7.0';
$SF_URI = 'urn:partner.soap.sforce.com';
$SF_PREFIX = 'sforce';
$SF_SOBJECT_URI = 'urn:sobject.partner.soap.sforce.com';

sub new {
   my $self = shift;
   return $self->login(@_);
}

sub login {

   my $class = shift;
   my ( %params ) = @_;

   my $sforce = undef;

   my $self = {
               sf_user => $params{'username'},
               sf_pass => $params{'password'},
               sf_sid => undef, #session ID
   };

   unless ( defined $params{'username'} and length $params{'username'} ) {
       $self->{errmsg} = "username parameter required";
       return undef;
   }

   unless ( defined $params{'password'} and length $params{'password'} ) {
       $self->{errmsg} = "password parameter required";
       return undef;
   }

   my $client = get_client();
   my $result = $client->login(
           SOAP::Data->name( 'username' => $self->{'sf_user'} ),
           SOAP::Data->name( 'password' => $self->{'sf_pass'} )
         );

   if ( $result->fault() ) {
        $self->{errmsg} = $result->faultstring();
        return undef;
   }

   $self->{'sforce'} = $client;
   $self->{'sf_sid'} = $result->valueof('//loginResponse/result/sessionId');
   $self->{'sf_uid'} = $result->valueof('//loginResponse/result/userId');
   $SF_PROXY = $result->valueof('//loginResponse/result/serverUrl');

   bless $self, $class;
   return $self;
}

sub get_session_header {
    my ( $self ) = @_;
    return SOAP::Header
        ->name( 'SessionHeader' => 
             \SOAP::Header->name(
                'sessionId' => $self->{'sf_sid'}
            )
        )
        ->uri( $SF_URI )
        ->prefix( $SF_PREFIX );
}

sub get_client {
    my ( $readable ) = @_;
    $readable = ( $readable )? 1 : 0;

    my $client = SOAP::Lite
        ->readable( $readable )
        ->deserializer( CRMConnector::Deserializer->new )
        ->serializer( CRMConnector::Serializer->new )
        ->on_action( sub { return '""' } )
        ->uri( $SF_URI )
        ->proxy( $SF_PROXY );
    return $client;
}

sub query {

   my $self = shift;
   my (%params) = @_;

   my @rows  = ();   

   my $limit = SOAP::Header
       ->name( 'QueryOptions' => 
           \SOAP::Header->name(
              'batchSize' => $params{'limit'}
           )
       )
       ->prefix( $SF_PREFIX )
       ->uri( $SF_URI );

   my $client = get_client(1);

   my $result = $client->query(
                 SOAP::Data->name( 'queryString' => $params{'query'} ),
                 $limit,
                 $self->get_session_header()
                );

   if ($result->fault()) {
      $self->{errmsg} = $result->faultstring();
      return undef;
   }

   push @rows, $result->valueof( '//queryResponse/result/records' )
            if ( $result->valueof( '//queryResponse/result/size' ) > 0 );

   if ($params{limit} && $params{limit} == 1){
     return \@rows;
   }

   #we get the results more than given limit... continue getting them
   my $done = $result->valueof( '//queryResponse/result/done' );
   my $ql = $result->valueof( '//queryResponse/result/queryLocator' );

   while ( $done eq 'false' ) {
       $result = $self->queryMore( queryLocator => $ql,
                                   limit => $params{limit} );
       if ( $result->fault() ) {
           $self->{errmsg} = $result->faultstring();
                return undef;
       }
       $done = $result->valueof( '//queryMoreResponse/result/done' );
       $ql = $result->valueof( '//queryMoreResponse/result/queryLocator' );
       push @rows, $result->valueof( '//queryMoreResponse/result/records' )
            if ( $result->valueof( '//queryMoreResponse/result/size' ) );
   }

   return \@rows;
}

sub queryMore {
    my $self = shift;
    my %params = @_;

    my $limit = SOAP::Header
        ->name( 'QueryOptions' => 
            \SOAP::Header->name(
                'batchSize' => $params{'limit'}
            )
        )
        ->prefix( $SF_PREFIX )
        ->uri( $SF_URI );

    my $client = get_client();
    my $r = $client->queryMore(
        SOAP::Data->name( 'queryLocator' => $params{'queryLocator'} ),
        $limit,
        $self->get_session_header()
    );
 
    return $r;
}
 
sub get_fields {

   my ($self, $type) = @_;

   my %flds = ();
   my $client = get_client();
   
   my $result = $self->describeSObject(type => $type);

   if ($result->fault()) {
      $self->{errmsg} = $result->faultstring();
      return undef;
   }

   my $descobj = $result->valueof('//describeSObjectResponse/result');

   foreach my $f ( @{$descobj->{fields}} ){
      $flds{$f->{name}} = $f;
   }

   return \%flds;
}

sub get_field_list {

    my ($self, $type) = @_;
    
    my $flds = $self->get_fields($type);

    return keys %{$flds};
}

sub get_tables {

    my $self = shift;

    my $result = $self->describeGlobal();
    if ( $result->fault() ) {
        $self->{errmsg} = $result->faultstring();
        return undef;
    }

    my @globals = $result->valueof('//describeGlobalResponse/result/types');
    return @globals;
}

sub get_userinfo {

   my $self = shift;
   my $userid = shift;

   my $userinfo = {};
   my $userfields = join ",", $self->get_field_list("User");

   $userinfo = $self->retrieve( 'fields' => $userfields, 
                                'type' => "User", 
                                'ids' => [$userid]);

   return $userinfo;
}

sub get_case_info {

    my $self = shift;

    #TODO: add case info retrieve code
}

sub create_crm_case {

    my $self = shift;
    my %in = @_;

    my %caseinfo = %{$in{'izparams'}};

    my $result = $self->create( 'type' => 'Case',
                                %caseinfo );

    if ( $result->fault() ) {
        carp( $result->faultstring() );
        $self->{errmsg} = $result->faultstring();
        return undef;
    }

    return $result->valueof("//createResponse/result");
}

sub create_attachment {

    my $self = shift;
    my %in = @_;

    my $crm_caseid   = $in{'caseid'};
    my $crm_filename = $in{'filename'};
    my $contenttype  = $in{'mimetype'};
    my $crm_base64body  = $in{'body'}; #base64 binary Encoded file data.

    if ( !$crm_caseid ) {
       carp("Case id required to update a Case");
       return undef;
    }
    if ( !$crm_filename ) {
       carp("Expected 'filename' parameter");
       return undef;
    }
    if ( !$crm_base64body ) {
       carp("Base64 encoded body required to add attachment");
       return undef;
    }

    #TODO: add create crm case code
    my $result = $self->create( 'type' => 'Attachment',
                                'Body' => "$crm_base64body",
                                'Name' => "$crm_filename",
                                'ParentId' => "$crm_caseid",
                                'ContentType' => "$contenttype",
                              );

    if ( $result->fault() ) {
        carp( $result->faultstring() );
        $self->{errmsg} = $result->faultstring();
        return undef;
    }

    return $result->valueof("//createResponse/result");
}

sub update_crm_case {

    my $self = shift;
    my %in = @_;

    my $crm_caseid = $in{'caseid'};
    my $crm_caseno = $in{'caseno'};

    if ( !$crm_caseid && $crm_caseno ) {
       $crm_caseid = $self->get_crm_caseid('caseno' => $crm_caseno);
    }
    if ( !$crm_caseid ) {
       carp("Case id required to update a Case");
       return undef;
    }

    #TODO: add case fields    
    my %caseupdates = %{$in{'izparams'}};

    my $result = $self->update( 'id' => $crm_caseid,
                                'type' => 'Case',
                                %caseupdates );

    if ( $result->{errors} ) {
        $self->{errmsg} = "Error: update Case $crm_caseno Failed!";
        carp( $self->{errmsg} );
        #return undef;
    }
    return $result;
}

sub add_crm_casecomment {

    my $self = shift;
    my %in = @_;

    my $crm_caseid = $in{'caseid'};
    my $crm_caseno = $in{'caseno'};
    my $crm_comment = $in{'comment'};
    my $crm_ispublic = $in{'ispublic'};

    if ( !$crm_caseid && $crm_caseno ) {
       $crm_caseid = $self->get_crm_caseid('caseno' => $crm_caseno);
    }
    if ( !$crm_caseid ) {
        carp( "Expected a hash with key 'caseid'" );
        return undef;
    }
    if ( !$crm_comment ) {
        carp( "Expected a hash with key 'comment'" );
        return undef;
    }
    if ( !$crm_ispublic ) {
        carp( "Expected a hash with key 'ispublic'" );
        return undef;
    }

    $crm_comment =~ s/&/&amp;/g;

    my %casecomment = ( 'ParentId' => "$crm_caseid",
                        'CommentBody' => "$crm_comment",
                        'IsPublished' => "$crm_ispublic",
                       );

    my $result = $self->create( 'type' => 'CaseComment',
                                %casecomment );

    if ( $result->fault() ) {
        carp( $result->faultstring() );
        $self->{errmsg} = $result->faultstring();
        return undef;
    }
    return $result;
}

sub get_crm_field {

    my $self = shift;
    my (%in) = @_;

    my $crm_field = $in{'field'};
    my $crm_table = $in{'table'};
    my $crm_where = $in{'where'};

    if ( !$crm_field ) {
        carp( "Expected a hash with key 'field'" );
        return undef;
    }
    if ( !$crm_table ) {
        carp( "Expected a hash with key 'table'" );
        return undef;
    }

    my $query_str = "SELECT $crm_field FROM $crm_table";
       $query_str .= " WHERE $crm_where" if ($crm_where);

    my $row = $self->query('query' => $query_str, 'limit' => 1);
    
    if (!defined $row){
       carp("Unable to retrieve $crm_field from $crm_table");
    }
    return $row;
}

sub get_crm_commentid {

    my $self = shift;
    my (%in) = @_;

    my $crm_caseid = $in{'caseid'};
    my $crm_commentid = undef;

    if ( !$crm_caseid ) {
        carp( "Expected a hash with key 'caseid'" );
        return undef;
    }

    my $row = $self->get_crm_field( 'field' => 'Id', 
                                    'table' => 'CaseComment', 
                                    'where' => "ParentId='$crm_caseid'");
    
    if (defined $row){
    	$crm_commentid = $row->[0]{'Id'}->[0];
    }

    if (!defined $crm_commentid){
       carp("Unable to retrieve comment id");
    }
    return $crm_commentid;
}

sub get_crm_caseid {

    my $self = shift;
    my (%in) = @_;

    my $crm_caseno = $in{'caseno'};
    my $crm_caseid = undef;

    if ( !$crm_caseno ) {
        carp( "Expected a hash with key 'caseno'" );
        return undef;
    }

    my $row = $self->get_crm_field( 'field' => 'Id', 
                                    'table' => 'Case', 
                                    'where' => "CaseNumber='$crm_caseno'");
    if (defined $row){
    	$crm_caseid = $row->[0]{'Id'}->[0];
    }

    if (!defined $crm_caseid){
       carp("Unable to retrieve Case Id");
    }
    return $crm_caseid;
}

sub get_crm_caseno {

    my $self = shift;
    my (%in) = @_;

    my $crm_caseid = $in{'caseid'};
    my $crm_caseno = undef;

    if ( !$crm_caseid ) {
        carp( "Expected a hash with key 'caseid'" );
        return undef;
    }

    my $row = $self->get_crm_field( 'field' => 'CaseNumber', 
                                    'table' => 'Case', 
                                    'where' => "Id='$crm_caseid'");
    if (defined $row){
        if (ref $row->[0]{'CaseNumber'} eq "ARRAY"){
          $crm_caseno = $row->[0]{'CaseNumber'}->[0];
        } else {
          $crm_caseno = $row->[0]{'CaseNumber'};
        }
    }

    if (!defined $crm_caseno){
       carp("Unable to retrieve Case Number");
    }

    return $crm_caseno;
}

sub create {
    my $self = shift;
    my (%in) = @_;

    if ( !keys %in ) {
        carp( "Expected a hash of arrays." );
        return undef;
    }
    my $client = get_client(1);
    my $method = SOAP::Data
        ->name("create")
        ->prefix( $SF_PREFIX )
        ->uri( $SF_URI )
        ->attr( { 'xmlns:sfons' => $SF_SOBJECT_URI } );

    my $type = $in{'type'};
    delete($in{'type'});

    my @elems;
    foreach my $key (keys %in) {

        my $typ = $CRMConnector::Constants::TYPES{$type}->{$key};
        my $val = $in{$key};

        if ( $typ eq 'xsd:string' ) {
            #$val = $self->xml_encode( $in{$key} );
        }
        elsif ( !defined $typ || length($typ) == 0 ) {
            $typ = 'xsd:string';
            #$val = $self->xml_encode( $in{$key} );
        }

        push @elems, SOAP::Data->prefix('sfons')
            ->name($key => $in{$key})
            ->type($typ);
    }

    my $r = $client->call(
        $method => 
            SOAP::Data->name('sObjects' => \SOAP::Data->value(@elems))
                ->attr( { 'xsi:type' => 'sfons:'.$type } ),
            $self->get_session_header()
    );

    if ( $r->fault() ) {
        carp( $r->faultstring() );
        return undef;
    }
    return $r;
}

sub update {

    my $self = shift;
    my (%in) = @_;

    my $type = $in{'type'};
    delete($in{'type'});
    my $id = $in{'id'};
    delete($in{'id'});

    if ( !$type ) {
        carp( "Expected a hash with key 'type'" );
        return undef;
    }
    if ( !$id ) {
        carp( "Expected a hash with key 'id'" );
        return undef;
    }

    my @elems;
    push @elems,
        SOAP::Data
            ->prefix( $SF_PREFIX )
            ->name( 'Id' => $id )
            ->type( 'sforce:ID' );
    foreach my $key (keys %in) {
        push @elems,
            SOAP::Data
                ->prefix( $SF_PREFIX )
                ->name( $key => $in{$key} )
                ->type( $CRMConnector::Constants::TYPES{$type}->{$key} );
    }

    my $client = get_client(1);
    my $method = SOAP::Data
        ->name("update")
        ->prefix( $SF_PREFIX )
        ->uri( $SF_URI )
        ->attr( { 'xmlns:sfons' => $SF_SOBJECT_URI } );

    my $r = $client->call(
        $method => SOAP::Data
            ->name( 'sObjects' => \SOAP::Data->value( @elems ) )
            ->attr( { 'xsi:type' => 'sforce:'.$type } ),
        $self->get_session_header()
    );

    return $r->valueof('//updateResponse/result') if ($r);
}

sub retrieve {
    my $self = shift;
    my (%in) = @_;

    $in{'limit'} = 500
        if ( !defined $in{'limit'} || $in{'limit'} !~ m/^\d+$/ );
    if ( $in{'limit'} < 1 || $in{'limit'} > 2000 ) {
        carp( "A query's limit cannot exceed 2000. 500 is default." );
        return undef;
    }
    if ( !defined $in{'fields'} || !length $in{'fields'} ) {
        carp( "Hash with key 'fields' expected." );
        return undef;
    }
    if ( !defined $in{'fields'} || !length $in{'fields'} ) {
        carp( "Hash with key 'fields' expected." );
        return undef;
    }
    if ( !defined $in{'type'} || !length $in{'type'} ) {
        carp( "Hash with key 'type' expected." );
        return undef;
    }

    my @elems;
    my $client = get_client(1);
    my $method = SOAP::Data
        ->name( "retrieve" )
        ->prefix( $SF_PREFIX )
        ->uri( $SF_URI );
    foreach my $id ( @{ $in{'ids'} } ) {
        push(
            @elems,
            SOAP::Data
                ->prefix( $SF_PREFIX )
                ->name('ids' => $id)
                ->type('xsd:string')
        );
    }
    my $r = $client->call(
        $method => SOAP::Data->prefix( $SF_PREFIX )
            ->name( 'fieldList' => $in{'fields'} )
            ->type( 'xsd:string'), 
        SOAP::Data->prefix( $SF_PREFIX )
            ->name( 'sObjectType' => $in{'type'} )
            ->type( 'xsd:string' ),
        @elems,
        $self->get_session_header()
    );

    if ( $r->fault() ) {
        carp( $r->faultstring() );
        $self->{errmsg} = $r->faultstring();
        return undef;
    }

    return $r->valueof('//retrieveResponse/result');
}

sub search {
    my $self = shift;
    my (%in) = @_;

    if ( !defined $in{'searchString'} || !length $in{'searchString'} ) {
        carp( "Expected hash with key 'searchString'" );
        return undef;
    }
    my $client = get_client(1);
    my $method = SOAP::Data
        ->name( "search" )
        ->prefix( $SF_PREFIX )
        ->uri( $SF_URI );
    my $r = $client->call(
        $method => SOAP::Data->prefix( $SF_PREFIX )
            ->name( 'searchString' => $in{'searchString'} )
            ->type( 'xsd:string' ),
        $self->get_session_header()
    );
    if ( $r->fault() ) {
        carp( $r->faultstring() );
        $self->{errmsg} = $r->faultstring();
    }
    return $r;
}


sub describeSObject {
    my $self = shift;
    my (%in) = @_;

    if ( !defined $in{'type'} or !length $in{'type'} ) {
        carp("Expected hash with key 'type'");
        return undef;
    }

    my $client = get_client(1);
    my $method = SOAP::Data
        ->name( "describeSObject" )
        ->prefix( $SF_PREFIX )
        ->uri( $SF_URI );

    my $r = $client->call(
        $method => SOAP::Data->prefix( $SF_PREFIX )
            ->name( 'sObjectType' => $in{'type'} )
            ->type( 'xsd:string' ), 
        $self->get_session_header()
    );
    if ( $r->fault() ) {
        carp( $r->faultstring() );
        return undef;
    }
    return $r;
}

sub describeGlobal {
    my $self = shift;

    my $client = get_client(1);
    my $method = SOAP::Data
        ->name("describeGlobal")
        ->prefix( $SF_PREFIX )
        ->uri( $SF_URI );

    my $r = $client->call(
        $method,
        $self->get_session_header()
    );
    if ( $r->fault() ) {
        carp( $r->faultstring() );
        return undef;
    }
    return $r;
}

###
package CRMConnector::Serializer;
{
    use strict;
    use warnings;
    use SOAP::Lite;
    use vars qw( @ISA );
    @ISA = qw( SOAP::Serializer );

    #**************************************************************************
    # encode_object( $object, $name, $type, $attr )
    #   -- overloaded encode_object() function to take care of problem
    #     with xsi:nil="true" attribute of tags that have no attributes
    #**************************************************************************
    sub encode_object {
        my ( $self, $object, $name, $type, $attr ) = @_;
        #print "\n\t\t",$name," *********************************************\n" if $name;
        if ( defined $attr->{'xsi:nil'} ) {
            delete $attr->{'xsi:nil'};
            return [$name, {%$attr}];
        }
        return $self->SUPER::encode_object( $object, $name, $type, $attr );
    }
}

###
package CRMConnector::Deserializer;
{
    use strict;
    use warnings;
    use SOAP::Lite;

    use vars qw( @ISA );

    @ISA = qw( SOAP::Deserializer );
    use strict 'refs';

    #**************************************************************************
    # new()
    #   -- constructor
    #**************************************************************************
    sub new {
        my $class  = shift;
        my $self = $class->SUPER::new(@_);
        my (%params) = @_;
        return $self;
    }

    BEGIN {
        use vars qw($XSD_NSPREFIX $XSI_NSPREFIX $SOAPENV_NSPREFIX 
	      $SOAPENC_NSPREFIX $NSPREFIX);

        $XSD_NSPREFIX="xsd";
        $XSI_NSPREFIX="xsi";
        $SOAPENV_NSPREFIX="SOAP-ENV";
        $SOAPENC_NSPREFIX="SOAP-ENC";
        $NSPREFIX="wsisup";
  
        no strict 'refs';
        for my $class ( qw(LoginResult) ) {
            my $method_name = "as_".$class;
            my $class_name = "CRMConnector::".$class;
            my $method_body = <<END_OF_SUB;
            
            sub $method_name {
                my (\$self,\$f,\$name,\$attr) = splice(\@_,0,4);
                my \$ns = pop;
                my \$${class} = CRMConnector::${class}->new;
                foreach my \$elem (\@_) {
                    \$elem = shift \@\$elem if (ref(\$elem->[0]) eq 'ARRAY');
                    my (\$name2, \$attr2, \$value2, \$ns2) = splice(\@{\$elem},0,4);
                    my (\$pre2,\$type2) = (\${attr2}->{\$XSI_NSPREFIX.":type"} =~ /([^:]*):(.*)/);
                    if (\$pre2 && \$pre2 eq \$XSD_NSPREFIX) {
                        \$${class}->{'_'.\$name2} = \$value2;
                    }
                    else {
                        my \$cmd = '\$self->as_'.\$type2.'(\$f,\@\$value2);';
                        \$${class}->{'_'.\$name2} = eval \$cmd;
                    }
                }
                return \$${class};
            }
END_OF_SUB
            #    print STDERR $method_body;
            #    *$method_name = eval $method_body;
            eval $method_body;
        }
    }

    #**************************************************************************
    # as_Array()
    #   -- returns the data as an array
    #**************************************************************************
    sub as_Array {
        my $self = shift;
        my $f = shift;
        my @Array;
        foreach my $elem (@_) {
            my ($name, $attr, $value, $ns) = splice(@$elem,0,4);
            my $attrv = ${attr}->{$XSI_NSPREFIX.":type"};
            my ($pre,$type) = ($attrv =~ /([^:]*):(.*)/);
            my $result;
            if ($pre eq $XSD_NSPREFIX) {
                $result = $value;
            }
            else {
                my $cmd = '$self->as_'.$type.'(1, $name, $attr, @$value, $ns );';
                #	    print STDERR $cmd . "\n";
                $result = eval $cmd;
            }
            push (@Array, $result);
        }
        return \@Array;
    }
}

###
package CRMConnector::Constants;

use strict;
use warnings;

BEGIN {
    use vars qw(%TYPES %IzCrmFlds);
    %TYPES = (
        'Case' => {
            'AssetId'                 => 'xsd:string',
            'CaseNumber'              => 'xsd:string',
            'ClosedDate'              => 'xsd:dateTime',
            'ContactId'               => 'xsd:string',
            'CreatedById'             => 'xsd:string',
            'CreatedDate'             => 'xsd:dateTime',
            'Description'             => 'xsd:string',
            'EngineeringReqNumber__c' => 'xsd:string',
            'IsClosed'                => 'xsd:boolean',
            'IsEscalated'             => 'xsd:boolean',
            'LastModifiedById'        => 'xsd:string',
            'LastModifiedDate'        => 'xsd:dateTime',
            'Origin'                  => 'xsd:string',
            'OwnerId'                 => 'xsd:string',
            'PotentialLiability__c'   => 'xsd:string',
            'Priority'                => 'xsd:string',
            'Product__c'              => 'xsd:string',
            'Reason'                  => 'xsd:string',
            'SLAViolation__c'         => 'xsd:string',
            'Status'                  => 'xsd:string',
            'Subject'                 => 'xsd:string',
            'SuppliedCompany'         => 'xsd:string',
            'SuppliedEmail'           => 'xsd:string',
            'SuppliedName'            => 'xsd:string',
            'SuppliedPhone'           => 'xsd:string',
            'SystemModstamp'          => 'xsd:dateTime',
            'Type'                    => 'xsd:string',
            'Primary_Xref_PCN__c'     => 'xsd:string',
        },
        'CaseComment' => {
            'CommentBody'    => 'xsd:string',
            'CreatedById'    => 'xsd:string',
            'CreatedDate'    => 'xsd:dateTime',
            'IsPublished'    => 'xsd:boolean',
            'ParentId'       => 'xsd:string',
            'SystemModstamp' => 'xsd:dateTime',
        },
        'Attachment' => {
            'Body'             => 'xsd:base64Binary',
            'BodyLength'       => 'xsd:int',
            'ContentType'      => 'xsd:string',
            'CreatedById'      => 'xsd:string',
            'CreatedDate'      => 'xsd:dateTime',
            'IsPrivate'        => 'xsd:boolean',
            'LastModifiedById' => 'xsd:string',
            'LastModifiedDate' => 'xsd:dateTime',
            'Name'             => 'xsd:string',
            'OwnerId'          => 'xsd:string',
            'ParentId'         => 'xsd:string',
            'SystemModstamp'   => 'xsd:dateTime',
        },
    );

    %IzCrmFlds = (
                 'priority' => 'Priority',
                 'short_desc' => 'Subject',
                 'issue_status' => 'Status',
                 'comment' => 'Description',
               );
}

1;
